﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using System.Web;

using MiniTwitter.Extensions;
using MiniTwitter.Net.Twitter;

namespace MiniTwitter.Net
{
    class TwitterClient
    {
        static TwitterClient()
        {
            ServicePointManager.Expect100Continue = false;
        }

        public TwitterClient()
        {
            RetryCount = 5;
        }

        private readonly object thisLock = new object();

        private NetworkCredential credential;

        private static readonly Regex schemaRegex = new Regex(@"(https?:\/\/[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        private static readonly Regex messageRegex = new Regex(@"^d\s([a-zA-Z_0-9]+)\s", RegexOptions.Compiled | RegexOptions.IgnoreCase);

        private static readonly Regex unescapeRegex = new Regex("&([gl]t);", RegexOptions.Compiled | RegexOptions.IgnoreCase);

        public static string Unescape(string text)
        {
            return unescapeRegex.Replace(text, match => match.Groups[1].Value == "gt" ? ">" : "<");
        }

        private const string clientName = "MiniTwitter";

        public IWebProxy Proxy
        {
            get { return WebRequest.DefaultWebProxy; }
            set { WebRequest.DefaultWebProxy = value; }
        }

        public User LoginedUser { get; private set; }

        public bool ConvertShortUrl { get; set; }

        public string Footer { get; set; }

        public int RetryCount { get; set; }

        public bool IsLogined
        {
            get
            {
                lock (thisLock)
                {
                    return LoginedUser != null;
                }
            }
        }
		
        private long? recentId;

        public Status[] RecentTimeline
        {
            get { return IsLogined ? GetStatuses("http://twitter.com/statuses/friends_timeline.xml?count=200", ref recentId) : Statuses.Empty; }
        }

        private long? repliesId;

        public Status[] RepliesTimeline
        {
            get { return IsLogined ? GetStatuses("http://twitter.com/statuses/mentions.xml?count=20", ref repliesId) : Statuses.Empty; }
        }

        private long? archiveId;

        public Status[] ArchiveTimeline
        {
            get { return IsLogined ? GetStatuses("http://twitter.com/statuses/user_timeline.xml", ref archiveId) : Statuses.Empty; }
        }

        private long? receivedId;

        public DirectMessage[] ReceivedMessages
        {
            get { return IsLogined ? GetMessages("http://twitter.com/direct_messages.xml", ref receivedId) : DirectMessages.Empty; }
        }

        private long? sentId;

        public DirectMessage[] SentMessages
        {
            get { return IsLogined ? GetMessages("http://twitter.com/direct_messages/sent.xml", ref sentId) : DirectMessages.Empty; }
        }

        public Status[] Favorites
        {
            get { return IsLogined ? GetStatuses("http://twitter.com/favorites.xml") : Statuses.Empty; }
		}

		#region added by yuki.
		public SearchResult[] SearchResult {
			get {
				return (IsLogined && Properties.Settings.Default.SearchString != "") ?
					GetSearchResults("", Properties.Settings.Default.SearchString, ref recentId) : new SearchResult[] {};
			}
		}
		#endregion

		public User[] Friends
        {
            get { return IsLogined ? GetUsers("http://twitter.com/statuses/friends.xml") : Users.Empty; }
        }

        public User[] Followers
        {
            get { return IsLogined ? GetUsers("http://twitter.com/statuses/followers.xml") : Users.Empty; }
        }

        public event EventHandler<UpdateEventArgs> Updated;

        public event EventHandler UpdateFailure;

        public bool? Login(string userName, string password)
        {
            lock (thisLock)
            {
                Logout();
                if (userName.IsNullOrEmpty() || password.IsNullOrEmpty())
                {
                    LoginedUser = null;
                    return null;
                }
                credential = new NetworkCredential(userName, password);
                LoginedUser = Validate();
                return LoginedUser != null;
            }
        }

        public void Logout()
        {
            lock (thisLock)
            {
                credential = null;
                LoginedUser = null;
                recentId = null;
                repliesId = null;
                archiveId = null;
                receivedId = null;
                sentId = null;
            }
        }

        public void Update(string text)
        {
            Update(text, 0);
        }

        public void Update(string text, long? replyId)
        {
            if (ConvertShortUrl)
            {
                text = schemaRegex.Replace(text,
                                             match =>
                                             match.Groups[1].Length < 30 ? match.Groups[1].Value : BitlyHelper.ConvertTo(match.Groups[1].Value));
            }
            Match m = messageRegex.Match(text);
            if (m.Success)
            {
                string user = m.Groups[1].Value;
                text = text.Substring(m.Length);
                UpdateMessage(user, text);
            }
            else
            {
                UpdateStatus(text, replyId);
            }
        }

        public bool ReTweet(long id)
        {
            var request = CreateHttpRequest(string.Format("http://twitter.com/statuses/retweet/{0}.xml", id));
            request.Method = "POST";
            try
            {
                using (request.GetResponse())
                {
                    return true;
                }
            }
            catch
            {
                return false;
            }
        }

        private void UpdateStatus(string text, long? replyId)
        {
            if (!Footer.IsNullOrEmpty())
            {
                text += " " + Footer;
            }
            for (int i = 0; i < RetryCount; i++)
            {
                var request = CreateHttpRequest("http://twitter.com/statuses/update.xml");
                request.Method = "POST";
                request.ContentType = "application/x-www-form-urlencoded";
                try
                {
                    using (var stream = request.GetRequestStream())
                    {
                        using (var writer = new StreamWriter(stream, Encoding.ASCII))
                        {
                            writer.Write("source={0}&status={1}", clientName, text.UrlEncode());
                            if (replyId.HasValue)
                            {
                                writer.Write("&in_reply_to_status_id={0}", replyId.Value);
                            }
                        }
                    }
                    var response = request.GetResponse();
                    using (var stream = response.GetResponseStream())
                    {
                        var status = Serializer<Status>.Deserialize(stream);
                        status.IsAuthor = true;
                        if (Updated != null)
                        {
                            Updated(this, new UpdateEventArgs(status));
                        }
                        return;
                    }
                }
                catch { }
            }
            if (UpdateFailure != null)
            {
                UpdateFailure(this, EventArgs.Empty);
            }
        }

        private void UpdateMessage(string user, string text)
        {
            for (int i = 0; i < RetryCount; i++)
            {
                var request = CreateHttpRequest("http://twitter.com/direct_messages/new.xml");
                request.Method = "POST";
                request.ContentType = "application/x-www-form-urlencoded";
                try
                {
                    using (var stream = request.GetRequestStream())
                    {
                        using (var writer = new StreamWriter(stream, Encoding.ASCII))
                        {
                            writer.Write("user={0}&text={1}", user, text.UrlEncode());
                        }
                    }
                    var response = request.GetResponse();
                    using (var stream = response.GetResponseStream())
                    {
                        var message = Serializer<DirectMessage>.Deserialize(stream);
                        message.IsAuthor = true;
                        if (Updated != null)
                        {
                            Updated(this, new UpdateEventArgs(message));
                        }
                        return;
                    }
                }
                catch { }
            }
            if (UpdateFailure != null)
            {
                UpdateFailure(this, EventArgs.Empty);
            }
        }

        public bool Delete(ITwitterItem item)
        {
            for (int i = 0; i < RetryCount; i++)
            {
                if (item.IsMessage ? DeleteMessage(item.ID) : DeleteStatus(item.ID))
                {
                    return true;
                }
            }
            return false;
        }

        private bool DeleteStatus(long id)
        {
            var request = CreateHttpRequest(string.Format("http://twitter.com/statuses/destroy/{0}.xml", id));
            request.Method = "DELETE";
            try
            {
                using (request.GetResponse())
                {
                    return true;
                }
            }
            catch
            {
                return false;
            }
        }

        private bool DeleteMessage(long id)
        {
            var request = CreateHttpRequest(string.Format("http://twitter.com/direct_messages/destroy/{0}.xml", id));
            request.Method = "DELETE";
            try
            {
                using (request.GetResponse())
                {
                    return true;
                }
            }
            catch
            {
                return false;
            }
        }

        public bool Favorite(Status status)
        {
            for (int i = 0; i < RetryCount; i++)
            {
                if (status.Favorited ? DeleteFavorite(status.ID) : CreateFavorite(status.ID))
                {
                    return true;
                }
            }
            return false;
        }

        private bool CreateFavorite(long id)
        {
            var request = CreateHttpRequest(string.Format("http://twitter.com/favorites/create/{0}.xml", id));
            request.Method = "POST";
            try
            {
                using (request.GetResponse())
                {
                    return true;
                }
            }
            catch
            {
                return false;
            }
        }

        private bool DeleteFavorite(long id)
        {
            var request = CreateHttpRequest(string.Format("http://twitter.com/favorites/destroy/{0}.xml", id));
            request.Method = "DELETE";
            try
            {
                using (request.GetResponse())
                {
                    return true;
                }
            }
            catch
            {
                return false;
            }
        }

        private HttpWebRequest CreateHttpRequest(string url)
        {
            var request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "GET";
            request.Credentials = credential;
            request.PreAuthenticate = true;
            request.Accept = "text/xml, application/xml";
            request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
            return request;
        }

        private User Validate()
        {
            for (int i = 0; i < RetryCount; i++)
            {
                var request = CreateHttpRequest("http://twitter.com/account/verify_credentials.xml");
                try
                {
                    var response = request.GetResponse();
                    using (var stream = response.GetResponseStream())
                    {
                        return Serializer<User>.Deserialize(stream);
                    }
                }
                catch (InvalidOperationException)
                {
                    return null;
                }
                catch { }
            }
            return null;
        }

        private User GetUser(string name)
        {
            for (int i = 0; i < RetryCount; i++)
            {
                var request = CreateHttpRequest(string.Format("http://twitter.com/users/show/{0}.xml", name));
                try
                {
                    var response = request.GetResponse();
                    using (var stream = response.GetResponseStream())
                    {
                        return Serializer<User>.Deserialize(stream);
                    }
                }
                catch (InvalidOperationException)
                {
                    return null;
                }
                catch { }
            }
            return null;
        }

        private User[] GetUsers(string url)
        {
            for (int i = 0; i < 5; i++)
            {
                var request = CreateHttpRequest(url);
                try
                {
                    var response = request.GetResponse();
                    using (var stream = response.GetResponseStream())
                    {
                        return Serializer<Users>.Deserialize(stream).User;
                    }
                }
                catch (InvalidOperationException)
                {
                    return Users.Empty;
                }
                catch { }
            }
            return null;
        }

        private Status[] GetStatuses(string url)
        {
            long? temp = null;
            return GetStatuses(url, ref temp);
        }

        private Status[] GetStatuses(string url, ref long? since_id)
        {
            if (since_id.HasValue)
            {
                if (url.EndsWith("xml"))
                {
                    url += "?since_id=" + since_id.Value;
                }
                else
                {
                    url += "&since_id=" + since_id.Value;
                }
            }
            for (int i = 0; i < RetryCount; i++)
            {
                var request = CreateHttpRequest(url);
                try
                {
                    var response = (HttpWebResponse)request.GetResponse();
                    using (var stream = response.GetResponseStream())
                    {
                        var statuses = Serializer<Statuses>.Deserialize(stream);
                        if (statuses.Status == null)
                        {
                            return Statuses.Empty;
                        }
                        since_id = statuses.Status.First().ID;
                        Array.ForEach(statuses.Status, item => item.IsAuthor = item.Sender.ID == LoginedUser.ID);
                        return statuses.Status;
                    }
                }
                catch (WebException e)
                {
                    if (e.Status == WebExceptionStatus.ProtocolError)
                    {
                        var response = (HttpWebResponse)e.Response;
                        if (response.StatusCode == HttpStatusCode.NotModified)
                        {
                            return Statuses.Empty;
                        }
                    }
                }
                catch (InvalidOperationException)
                {
                    return Statuses.Empty;
                }
                catch { }
            }
            return null;
        }

        private DirectMessage[] GetMessages(string url, ref long? since_id)
        {
            if (since_id.HasValue)
            {
                if (url.EndsWith("xml"))
                {
                    url += "?since_id=" + since_id.Value;
                }
                else
                {
                    url += "&since_id=" + since_id.Value;
                }
            }
            for (int i = 0; i < RetryCount; i++)
            {
                var request = CreateHttpRequest(url);
                try
                {
                    var response = (HttpWebResponse)request.GetResponse();
                    using (var stream = response.GetResponseStream())
                    {
                        var messages = Serializer<DirectMessages>.Deserialize(stream);
                        if (messages.DirectMessage == null)
                        {
                            return DirectMessages.Empty;
                        }
                        since_id = messages.DirectMessage.First().ID;
                        Array.ForEach(messages.DirectMessage, item => item.IsAuthor = item.Sender.ID == LoginedUser.ID);
                        return messages.DirectMessage;
                    }
                }
                catch (WebException e)
                {
                    if (e.Status == WebExceptionStatus.ProtocolError)
                    {
                        var response = (HttpWebResponse)e.Response;
                        if (response.StatusCode == HttpStatusCode.NotModified)
                        {
                            return DirectMessages.Empty;
                        }
                    }
                }
                catch (InvalidOperationException)
                {
                    return DirectMessages.Empty;
                }
                catch { }
            }
            return null;
        }

		#region added by yuki.
		/// <summary>
		/// 
		/// for this method, I referenced http://www.dnknormark.net/post/Search-Twitter-from-C-using-LINQ-to-XML.aspx . Thanks
		/// </summary>
		/// <param name="url"></param>
		/// <param name="since_id">T.B.D.</param>
		/// <returns></returns>
		private SearchResult[] GetSearchResults(string url, string query, ref long? since_id) {
			string langStr = Properties.Settings.Default.IsSearchOnlyJapanese ? "lang=ja&" : "";
			string nameRegexStr = @"(?<ScreenName>.*)\((?<Name>.*?)\)$";

			for (int i = 0; i < RetryCount; i++) {
				try {
					XDocument results = XDocument.Load(
						String.Format("http://search.twitter.com/search.atom?{0}q={1}", langStr, HttpUtility.UrlEncode(query)));
					XNamespace ns = "http://www.w3.org/2005/Atom";
					var q =
						from tweet in results.Descendants(ns + "entry")
						select new SearchResult {
							Text = (string)tweet.Element(ns + "title"),
							IdSTring = (string)tweet.Element(ns + "id"),
							CreatedAt = DateTime.Parse((string)tweet.Element(ns + "published")),
							Sender = (from author in tweet.Descendants(ns + "author")
								select new User() {
									ScreenName = Regex.Match(
										(string)author.Element(ns + "name"),
										nameRegexStr).Groups["ScreenName"].Value,
									Name = Regex.Match(
										(string)author.Element(ns + "name"),
										nameRegexStr).Groups["Name"].Value,
									Url = (string)author.Element(ns + "uri"),
										ImageUrl =
											(from link in tweet.Elements(ns + "link")
											where (string)link.Attribute("rel") == "image"
											select (string)link.Attribute("href")).First(),
									Protected = false,
								}).First()
						};

						//select new {
						//    Text = (string)tweet.Element(ns + "title"),
						//    Html = (string)tweet.Element(ns + "content"),
						//    DatePublished = DateTime.Parse((string)tweet.Element(ns + "published")),
						//    Id = (string)tweet.Element(ns + "id"),
						//    Link = (string)tweet.Elements(ns + "link")
						//    .Where(link => (string)link.Attribute("rel") == "alternate")
						//    .Select(link => (string)link.Attribute("href"))
						//    .First(),
						//    Author = (from author in tweet.Descendants(ns + "author")
						//              select new {
						//                  Name = (string)author.Element(ns + "name"),
						//                  Uri = (string)author.Element(ns + "uri"),
						//              }).First()
						//};
					return q.ToArray();
				} catch {
				}
			}
			return null;
		}

		#endregion
	}
}